Skip to content

fix: weight:0 un-gates criteria + configurable retry policy#34

Open
uipreliga wants to merge 1 commit into
mainfrom
fix/weight-zero-gating-and-retry-policy
Open

fix: weight:0 un-gates criteria + configurable retry policy#34
uipreliga wants to merge 1 commit into
mainfrom
fix/weight-zero-gating-and-retry-policy

Conversation

@uipreliga

Copy link
Copy Markdown
Collaborator

Summary

Three independent fixes surfaced by a prior review, plus the code-review follow-ups on top of them.

1. weight: 0 now makes a criterion informational

Previously a weight: 0 criterion contributed nothing to the weighted score but could still single-handedly flip a task to FAILURE — the docs told authors to also set pass_threshold: 0, which was easy to miss.

  • New BaseSuccessCriterion.is_gating (weight > 0.0) is the single source of truth for "does a low score here fail the task".
  • EvaluationResult.all_criteria_passed and the coder-eval evaluate exit code now gate over is_gating criteria only. A weight: 0 miss no longer flips final_status or the exit code.
  • A validator rejects weight: 0 combined with stop_when or suite_thresholds — arming a non-gating criterion for a pass/fail gate (early-stop gate, or the across-row suite gate that drives the run exit code) is incoherent and is now caught at load time.

2. Configurable retry policy

  • New RunLimits.retry (RetryPolicy: max_retries / initial_delay / backoff_multiplier, all optional) exposed via -D run_limits.retry.max_retries=0 to fail fast while debugging.
  • All retry decisions resolve through one seam, errors/retry.py::resolve_retry_config. A category that is non-retryable in the built-in table (auth/billing) is never made retryable by a per-run override — retryability is a property of the error, not the run.
  • Persisted error_details.is_retryable / retry_delay_seconds in task.json now reflect the run's actual policy instead of the built-in defaults.

3. Doc drift + evalboard

  • Dropped the non-existent rephrase prompt mutation from the catalog.
  • Corrected the stale "max_memory_mb NOT enforced" claim (it maps to docker run --memory; max_cpus/max_pids--cpus/--pids-limit).
  • reports.py failure-reason sampling uses pass_threshold, not score < 1.0.
  • evalboard: /trends and /watchlist now ship in the OSS edition (they derive purely from run artifacts); only /path-to-ga stays internal-gated.

Test plan

  • make verify green — 3437 passed, 2 skipped, 90.90% coverage, ruff + pyright + custom CE lint clean.
  • New regression tests: weight: 0 un-gating semantics, the stop_when/suite_thresholds rejections, retry-policy resolution + fail-fast, error_details policy fidelity, and a coder-eval evaluate exit-code test (with fixture) proving an informational miss doesn't fail the run.

Follow-ups (not in this PR)

Display-only sites (terminal PASS/FAIL label, reports_html, evalboard result pill, checker log line) still label a below-threshold weight: 0 criterion as failed. final_status and both exit codes are correct; a clean fix needs a coordinated gating marker on CriterionResult/CriterionSummary + the evalboard DTO, so it's deferred to avoid partial header-vs-list contradictions.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @uipreliga's task in 1m 59s —— View job


Code Review In Progress

Review Tasks

  • Read review guidelines from .github/code_review.md
  • Read project conventions from CLAUDE.md
  • Get full PR diff
  • Review each changed file for context
  • Perform cross-file consistency checks
  • Analyze design decisions
  • Check for missing elements
  • Provide structured feedback

Starting review...

@akshaylive akshaylive left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The weight 0 fix LGTM. The configurable retry policy should ideally be a separate PR. Additionally the retry policy configuration shouldn't be in "run_limits" (retry policy is not really a limit, so a separate top level field is better). I haven't looked too deep into the impl but would this clear the working directory between retries?

@bai-uipath

Copy link
Copy Markdown
Collaborator

The evalboard change moves /trends and /watchlist into the OSS edition on the grounds that they "derive purely from run artifacts." That conflates two different bars: "runs without internal config" (portability — true) vs. "produces something meaningful for a generic adopter" (product fit — not really). Portability is cleared; product fit is the unaddressed concern, and it's the one that matters for an OSS-credibility play. Reasons these shouldn't ship OSS as-is:

  • They assume an operating model OSS users won't have. The value prop — "last 10 runs," regression (recent-vs-prev pass rate), streaks, volatility sparklines, "never passed across the window" — assumes a recurring scheduled run of a stable suite (the UiPath nightly model). An adopter doing a handful of ad-hoc experiment runs gets degenerate output. Not hardcoded — assumed.
  • Graceful ≠ useful. Several panels go blank without internal enrichment: dominantFailureTags needs a review index (readRunReviewIndex, not an OSS-core artifact), the maturity column needs matureSkipped (not produced by OSS core — always false in a bare clone), component-SHA attribution needs CI wiring. A new adopter lands on a half-empty Watchlist and reads it as broken — worse than not shipping it.

@uipreliga
uipreliga force-pushed the fix/weight-zero-gating-and-retry-policy branch 2 times, most recently from ee9aa58 to c746087 Compare July 22, 2026 21:31
@uipreliga

Copy link
Copy Markdown
Collaborator Author

@akshaylive thanks — all three points taken.

1. Separate PR. Done. This PR is now the weight: 0 fix alone (plus the small doc/report/evalboard corrections); the retry work moved to #44, stacked on this branch. Rebased onto latest main while I was in there.

2. retry shouldn't live under run_limits. Agreed, and moved to a top-level retry: block in #44. Your framing is the right one: run_limits holds caps that abort a run, while a retry policy is how hard the run tries to survive a transient error. Worth noting run_limits had already drifted in this direction (count_cached_input, count_cache_creation, stop_early, expected_turns aren't caps either) — which I took as an argument for not adding a fifth exception. It's now a first-class 4th -D root next to agent/run_limits/sandbox, so -D retry.max_retries=0 merges through the same resolve_root engine at every config layer.

3. "Would this clear the working directory between retries?" No, it does not — good catch, and it's a real pre-existing gap rather than something the retry knob introduced:

  • Retry wraps three seams in orchestrator.py: sandbox setup, agent start, and _communicate_with_retry.
  • On a communicate retry the sandbox is untouched — files the crashed attempt already wrote survive into the retried attempt, and the same agent/SDK session is reused, so the re-sent prompt lands in a conversation that already contains the failed trajectory.
  • The crashed partial TurnRecord is also drained onto the result (_on_attempt_failure), so trajectory-observing criteria — command_executed, commands_efficiency, skill_triggered — see both attempts. commands_efficiency in particular scores worse purely because a retry happened.

I chose to document this rather than add a reset seam: clearing the sandbox mid-task would discard work the agent had already done and budget already spent, and preserving partials is deliberate (crash forensics). #44 adds a retry section to TASK_DEFINITION_GUIDE.md spelling out the in-place semantics, with retry.max_retries: 0 as the escape hatch when you want a clean single-attempt trajectory. If you'd rather have an explicit reset option, I'm happy to add it — say the word and I'll do it in #44.

- weight: 0 now makes a criterion purely informational — excluded from
  both the weighted score AND the pass/fail gate (previously it could
  still flip a task to FAILURE). New BaseSuccessCriterion.is_gating drives
  the single-source gate (all_criteria_passed) and the `coder-eval
  evaluate` exit code. A validator rejects weight:0 combined with
  stop_when or suite_thresholds (arming a non-gating criterion for a
  pass/fail gate is incoherent).
- reports.py failure-reason sampling uses pass_threshold, not score < 1.0.
- Doc drift: dropped the non-existent `rephrase` mutation; corrected the
  stale "max_memory_mb NOT enforced" claim (docker --memory/--cpus/--pids-limit).

Split out per review: the configurable retry policy moved to #44, and the
evalboard OSS-edition change is dropped entirely (see @bai-uipath's review
comment — the panels need core-produced fields that do not exist yet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@uipreliga
uipreliga force-pushed the fix/weight-zero-gating-and-retry-policy branch from c746087 to 8c02dc6 Compare July 22, 2026 21:40
uipreliga added a commit that referenced this pull request Jul 22, 2026
Split out of #34 per review, with the placement feedback applied.

- New RetryPolicy (models/retry.py): max_retries / initial_delay /
  backoff_multiplier, all optional. It lives at the task's TOP LEVEL
  (`retry:`), not under `run_limits` — `run_limits` holds caps that abort a
  run, while a retry policy is how hard the run tries to survive a transient
  error.
- `retry` is a first-class 4th `-D` root alongside agent/run_limits/sandbox:
  `-D retry.max_retries=0` to fail fast while debugging. It merges through
  the SAME resolve_root engine at every layer (experiment defaults, task,
  variant, CLI), so the unification invariant holds — covered by new
  test_merge_unification cases.
- All retry decisions resolve through one seam, errors/retry.py::
  resolve_retry_config. A category that is non-retryable in the built-in
  table (auth/billing) is never made retryable by a per-run override —
  retryability is a property of the error, not the run.
- Persisted error_details.is_retryable / retry_delay_seconds now reflect the
  run's actual policy instead of the built-in defaults.
- Docs: TASK_DEFINITION_GUIDE gains a `retry` section that documents the
  placement rationale AND the in-place retry semantics raised in review —
  a retried communicate() re-sends into the same sandbox and session (the
  working directory is NOT reset), and the crashed attempt's partial turn is
  preserved, so trajectory-observing criteria see both attempts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@uipreliga

Copy link
Copy Markdown
Collaborator Author

@bai-uipath you're right, and I've dropped the evalboard change from this PR entirely rather than argue it.

The justification in my PR description — "they derive purely from run artifacts" — was true and answered the wrong question. Portability was the bar I checked; product fit is the bar that matters here, and I didn't check it. Your two specifics hold up when I trace them:

  • matureSkipped has no producer in src/. grep -rn mature_skipped src/ returns nothing — the Python core never emits mature_skipped, so lib/trends.ts falls through to ?? false on every task in a bare clone. The maturity column is structurally dead OSS-side, not just sparse.
  • dominantFailureTags needs the review index. app/watchlist/page.tsxloadRecentRuns (lib/overview.ts) → readRunReviewIndex (lib/reviews.ts), and review.json is produced by the coder-eval-review skill, not by core. A plain adopter gets an empty tag list.

So a new adopter lands on a Watchlist with a dead column and an empty failure-tag panel, and reasonably concludes the page is broken. That's worse than not shipping it, exactly as you said.

Dropping it also fixes a packaging problem @akshaylive flagged on the retry policy — this PR is now the weight: 0 fix alone, which is what it claims to be.

If we do want these OSS-side later, the prerequisite is core actually emitting the fields the panels read (mature_skipped at minimum, and some core-produced equivalent of the review index), which is its own piece of work rather than a nav-link change. Happy to open an issue for that if you think it's worth tracking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants